/*
 * Copyright (c) JForum Team
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, 
 * with or without modification, are permitted provided 
 * that the following conditions are met:
 * 
 * 1) Redistributions of source code must retain the above 
 * copyright notice, this list of conditions and the 
 * following  disclaimer.
 * 2)  Redistributions in binary form must reproduce the 
 * above copyright notice, this list of conditions and 
 * the following disclaimer in the documentation and/or 
 * other materials provided with the distribution.
 * 3) Neither the name of "Rafael Steil" nor 
 * the names of its contributors may be used to endorse 
 * or promote products derived from this software without 
 * specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT 
 * HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 
 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 
 * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 
 * IN CONTRACT, STRICT LIABILITY, OR TORT 
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
 * 
 * This file creation date: Mar 16, 2003 / 1:31:30 AM
 * The JForum Project
 * http://www.jforum.net
 */
package net.jforum.context.web;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import net.jforum.UrlPattern;
import net.jforum.UrlPatternCollection;
import net.jforum.context.RequestContext;
import net.jforum.context.SessionContext;
import net.jforum.exceptions.MultipartHandlingException;
import net.jforum.util.legacy.commons.fileupload.FileItem;
import net.jforum.util.legacy.commons.fileupload.FileUploadException;
import net.jforum.util.legacy.commons.fileupload.disk.DiskFileItemFactory;
import net.jforum.util.legacy.commons.fileupload.servlet.ServletFileUpload;
import net.jforum.util.legacy.commons.fileupload.servlet.ServletRequestContext;
import net.jforum.util.preferences.ConfigKeys;
import net.jforum.util.preferences.SystemGlobals;

import org.apache.commons.lang.StringUtils;

/**
 * @author Rafael Steil
 * @version $Id: WebRequestContext.java,v 1.15 2007/10/15 15:16:15 andowson Exp
 *          $
 */
public class WebRequestContext extends HttpServletRequestWrapper implements
	RequestContext {
    private Map query;

    /**
     * Default constructor.
     * 
     * @param superRequest
     *            Original <code>HttpServletRequest</code> instance
     * @throws IOException
     */
    public WebRequestContext(HttpServletRequest superRequest)
	    throws IOException {
	super(superRequest);

	this.query = new HashMap();
	boolean isMultipart = false;

	String requestType = superRequest.getMethod().toUpperCase();
	String contextPath = superRequest.getContextPath();
	String requestUri = this.extractRequestUri(
		superRequest.getRequestURI(), contextPath);
	String encoding = SystemGlobals.getValue(ConfigKeys.ENCODING);
	String servletExtension = SystemGlobals
		.getValue(ConfigKeys.SERVLET_EXTENSION);

	boolean isPost = "POST".equals(requestType);
	boolean isGet = !isPost;

	boolean isQueryStringEmpty = (superRequest.getQueryString() == null || superRequest
		.getQueryString().length() == 0);

	if (isGet && isQueryStringEmpty
		&& requestUri.endsWith(servletExtension)) {
	    superRequest.setCharacterEncoding(encoding);
	    this.parseFriendlyURL(requestUri, servletExtension);
	} else if (isPost) {
	    isMultipart = ServletFileUpload
		    .isMultipartContent(new ServletRequestContext(superRequest));

	    if (isMultipart) {
		this.handleMultipart(superRequest, encoding);
	    }
	}

	if (!isMultipart) {
	    boolean isAjax = "XMLHttpRequest".equals(superRequest
		    .getHeader("X-Requested-With"));

	    if (!isAjax) {
		superRequest.setCharacterEncoding(encoding);
	    } else {
		// Ajax requests are *usually* sent using
		// application/x-www-form-urlencoded; charset=UTF-8.
		// In JForum, we assume this as always true.
		superRequest.setCharacterEncoding("UTF-8");
	    }

	    String containerEncoding = SystemGlobals
		    .getValue(ConfigKeys.DEFAULT_CONTAINER_ENCODING);

	    if (isPost) {
		containerEncoding = encoding;
	    }

	    for (Enumeration e = superRequest.getParameterNames(); e
		    .hasMoreElements();) {
		String name = (String) e.nextElement();

		String[] values = superRequest.getParameterValues(name);

		if (values != null && values.length > 1) {
		    for (int i = 0; i < values.length; i++) {
			this.addParameter(
				name,
				new String(values[i]
					.getBytes(containerEncoding), encoding));
		    }
		} else {
		    this.addParameter(name, new String(superRequest
			    .getParameter(name).getBytes(containerEncoding),
			    encoding));
		}
	    }

	    if (this.getModule() == null && this.getAction() == null) {
		int index = requestUri.indexOf('?');

		if (index > -1) {
		    requestUri = requestUri.substring(0, index);
		}

		this.parseFriendlyURL(requestUri, servletExtension);
	    }
	}
    }

    /**
     * @param requestUri
     * @param servletExtension
     */
    private void parseFriendlyURL(String requestUri, String servletExtension) {
	requestUri = requestUri.substring(0, requestUri.length()
		- servletExtension.length());
	String[] urlModel = requestUri.split("/");

	int moduleIndex = 1;
	int actionIndex = 2;
	int baseLen = 3;

	UrlPattern url = null;

	if (urlModel.length >= baseLen) {
	    // <moduleName>.<actionName>.<numberOfParameters>
	    StringBuffer sb = new StringBuffer(64)
		    .append(urlModel[moduleIndex]).append('.')
		    .append(urlModel[actionIndex]).append('.')
		    .append(urlModel.length - baseLen);

	    url = UrlPatternCollection.findPattern(sb.toString());
	}

	if (url != null) {
	    if (url.getSize() >= urlModel.length - baseLen) {
		for (int i = 0; i < url.getSize(); i++) {
		    this.addParameter(url.getVars()[i], urlModel[i + baseLen]);
		}
	    }

	    this.addOrReplaceParameter("module", urlModel[moduleIndex]);
	    this.addParameter("action", urlModel[actionIndex]);
	} else {
	    this.addOrReplaceParameter("module", null);
	    this.addParameter("action", null);
	}
    }

    public SessionContext getSessionContext(boolean create) {
	return new WebSessionContext(this.getSession(true));
    }

    public SessionContext getSessionContext() {
	return new WebSessionContext(this.getSession());
    }

    /**
     * @param superRequest
     *            HttpServletRequest
     * @param encoding
     *            String
     * @throws UnsupportedEncodingException
     */
    private void handleMultipart(HttpServletRequest superRequest,
	    String encoding) throws UnsupportedEncodingException {
	String tmpPath = new StringBuffer(256)
		.append(SystemGlobals.getApplicationPath()).append('/')
		.append(SystemGlobals.getValue(ConfigKeys.TMP_DIR)).toString();

	File tmpDir = new File(tmpPath);
	boolean success = false;

	try {
	    if (!tmpDir.exists()) {
		tmpDir.mkdirs();
		success = true;
	    }
	} catch (Exception e) {
	    // We won't log it because the directory
	    // creation failed for some reason - a SecurityException
	    // or something else. We don't care about it, as the
	    // code below tries to use java.io.tmpdir
	}

	if (!success) {
	    tmpPath = System.getProperty("java.io.tmpdir");
	    tmpDir = new File(tmpPath);
	}

	ServletFileUpload upload = new ServletFileUpload(
		new DiskFileItemFactory(100 * 1024, tmpDir));
	upload.setHeaderEncoding(encoding);

	try {
	    List items = upload.parseRequest(superRequest);

	    for (Iterator iter = items.iterator(); iter.hasNext();) {
		FileItem item = (FileItem) iter.next();

		if (item.isFormField()) {
		    this.addParameter(item.getFieldName(),
			    item.getString(encoding));
		} else {
		    if (item.getSize() > 0) {
			// We really don't want to call addParameter(), as
			// there should not be possible to have multiple
			// values for a InputStream data
			this.query.put(item.getFieldName(), item);
		    }
		}
	    }
	} catch (FileUploadException e) {
	    throw new MultipartHandlingException(
		    "Error while processing multipart content: " + e);
	}
    }

    /**
     * @see javax.servlet.ServletRequestWrapper#getParameterValues(java.lang.String)
     */
    public String[] getParameterValues(String name) {
	Object value = this.getObjectParameter(name);

	if (value instanceof String) {
	    return new String[] { (String) value };
	}

	List l = (List) value;

	return l == null ? super.getParameterValues(name) : (String[]) l
		.toArray(new String[0]);
    }

    private String extractRequestUri(String requestUri, String contextPath) {
	// First, remove the context path from the requestUri,
	// so we can work only with the important stuff
	if (contextPath != null && contextPath.length() > 0) {
	    requestUri = requestUri.substring(contextPath.length(),
		    requestUri.length());
	}

	// Remove the "jsessionid" (or similar) from the URI
	// Probably this is not the right way to go, since we're
	// discarding the value...
	int index = requestUri.indexOf(';');

	if (index > -1) {
	    int lastIndex = requestUri.indexOf('?', index);

	    if (lastIndex == -1) {
		lastIndex = requestUri.indexOf('&', index);
	    }

	    if (lastIndex == -1) {
		requestUri = requestUri.substring(0, index);
	    } else {
		String part1 = requestUri.substring(0, index);
		requestUri = part1 + requestUri.substring(lastIndex);
	    }
	}

	return requestUri;
    }

    /**
     * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
     */
    public String getParameter(String parameter) {
	return (String) this.query.get(parameter);
    }

    /**
     * Gets an parameter that is a number. A call to
     * <code>Integer#parseInt(String)</code> is made to do the conversion
     * 
     * @param parameter
     *            The parameter name to get the value
     * @return int
     */
    public int getIntParameter(String parameter) {
	return Integer.parseInt(this.getParameter(parameter));
    }

    /**
     * Gets some request parameter as <code>Object</code>. This method may be
     * used when you have to get some value of a <i>multipart/form-data</i>
     * request, like a image of file. <br>
     * 
     * @param parameter
     *            String
     * @return Object
     */
    public Object getObjectParameter(String parameter) {
	return this.query.get(parameter);
    }

    public void addParameter(String name, Object value) {
	if (!this.query.containsKey(name)) {
	    this.query.put(name, value);
	} else {
	    Object currentValue = this.getObjectParameter(name);
	    List l;

	    if (!(currentValue instanceof List)) {
		l = new ArrayList();
		l.add(currentValue);
	    } else {
		l = (List) currentValue;
	    }

	    l.add(value);
	    this.query.put(name, l);
	}
    }

    public void addOrReplaceParameter(String name, Object value) {
	this.query.put(name, value);
    }

    /**
     * Gets the <i>action</i> of the current request.
     * 
     * An <i>Action</i> is the parameter name which specifies what next action
     * should be done by the system. It may be add or edit a post, editing the
     * groups, whatever. In the URL, the Action can the represented in two
     * forms:
     * <p>
     * <blockquote> <code>
     * http://www.host.com/webapp/servletName?module=groups&action=list
     * </code>
     * </blockquote>
     * <p>
     * or
     * <p>
     * <blockquote> <code>
     * http://www.host.com/webapp/servletName/groups/list
     * </code>
     * </blockquote>
     * <p>
     * In both situations, the action's name is "list".
     * 
     * @return String representing the action name
     */
    public String getAction() {
	return this.getParameter("action");
    }

    public void changeAction(String newAction) {
	if (this.query.containsKey("action")) {
	    this.query.remove("action");
	    this.query.put("action", newAction);
	} else {
	    this.addParameter("action", newAction);
	}
    }

    /**
     * Gets the <i>module</i> of the current request.
     * 
     * A <i>Module</i> is the parameter name which specifies what module the
     * user is requesting. It may be the group administration, the topics or
     * anything else configured module. In the URL, the Module can the
     * represented in two forms:
     * <p>
     * <blockquote> <code>
     * http://www.host.com/webapp/servletName?module=groups&action=list
     * </code>
     * </blockquote>
     * <p>
     * or
     * <p>
     * <blockquote> <code>
     * http://www.host.com/webapp/servletName/groups/list
     * </code>
     * </blockquote>
     * <p>
     * In both situations, the module's name is "groups".
     * 
     * @return String representing the module name
     */
    public String getModule() {
	return this.getParameter("module");
    }

    public Object getObjectRequestParameter(String parameter) {
	return this.query.get(parameter);
    }

    /**
     * @see javax.servlet.http.HttpServletRequestWrapper#getContextPath()
     */
    public String getContextPath() {
	String contextPath = super.getContextPath();
	String proxiedContextPath = SystemGlobals
		.getValue(ConfigKeys.PROXIED_CONTEXT_PATH);

	if (!StringUtils.isEmpty(proxiedContextPath)) {
	    contextPath = proxiedContextPath;
	}

	return contextPath;
    }

    /**
     * @see javax.servlet.ServletRequestWrapper#getRemoteAddr()
     */
    public String getRemoteAddr() {
	// We look if the request is forwarded
	// If it is not call the older function.
	String ip = super.getHeader("x-forwarded-for");

	if (ip == null) {
	    ip = super.getRemoteAddr();
	} else {
	    // Process the IP to keep the last IP (real ip of the computer on
	    // the net)
	    StringTokenizer tokenizer = new StringTokenizer(ip, ",");

	    // Ignore all tokens, except the last one
	    for (int i = 0; i < tokenizer.countTokens() - 1; i++) {
		tokenizer.nextElement();
	    }

	    ip = tokenizer.nextToken().trim();

	    if (ip.equals("")) {
		ip = null;
	    }
	}

	// If the ip is still null, we put 0.0.0.0 to avoid null values
	if (ip == null) {
	    ip = "0.0.0.0";
	}

	return ip;
    }
}
